|
このテクニカルノートでは、アイソクロナス転送方式の FireWire デバイス用に DCL プログラムを記述する際のアドバイスとヒントを提供します。
本稿は主に、アイソクロナス転送方式の FireWire デバイスとの間でデータを送受信するユーザ空間のコードを記述するデベロッパを対象にしています。
「Accessing Hardware From Applications」で解説している I/O Kit およびデバイスインタフェース全般の概念は、十分に理解しておいてください。
また、FireWire SDK for Mac OS X には、プログラミングに役立つサンプルとドキュメントが用意されています。現在の FireWire SDK は、ここから入手できます。
FireWire の仕様に関する詳細は、本稿では範囲を越えるため取り上げていません。詳細については、アップルの FireWire に関する技術文献および 1394 Trade Association のWeb サイトを参照してください。
[2003 年 6 月 21 日]
|
DCL とは何か
DCL (Datastream Control Language) は、データストリームへ、またはデータストリームからのデータの流れを制御するコマンドセットです。DCL プログラムは、いくつかの DCL コマンドを集めてリンクリストを構成したものです。DCL プログラムは特定のデータストリーム(アイソクロナスチャネルなど)と関連付けられ、これを制御します。
先頭に戻る
DCL コマンド
このセクションでは、DCL プログラムで使用できる DCL コマンドについて説明します。各コマンドは、IOFireWireLibDCLCommandPool インタフェースによって提供される AllocateXXXDCL メソッドを使って割り当てられます。「XXX 」は DCL コマンドを表しており、たとえば、AllocateSendPacketStartDCL というようになります。
-
SendPacketStart
-
データストリームに送信するパケットバッファの最初の部分(または全部)を指定します。アイソクロナスパケットヘッダが生成されます。
-
SendPacketWithHeaderStart
-
データストリームに送信するアイソクロナスパケットヘッダを含む、パケットバッファの最初の部分(または全部)を指定します。このバッファの先頭 4 バイトは、このパケットと一緒に送信する 1394 アイソクロナスパケットヘッダにする必要があります。
-
SendPacket
-
直前の
Send コマンドに付加する追加バイトを指定します。
-
ReceivePacketStart
-
データストリームからパケットの最初の部分(または全部)を受信するバッファを指定します。
-
ReceivePacket
-
データストリームから次のパケットを受信するバッファを指定します。
-
CallProc
-
このコマンドを実行すると、ユーザが指定するコールバック関数が呼び出されます。DCL プログラムはコールバックが戻ってくるのを待たず、CPU とは独立に実行を続けます。詳細については「DCL プログラムの構造と実行」を参照してください。
-
Label
-
DCL プログラム内で、
Jump コマンドのジャンプ先として使用できる位置を指定します。
-
Jump
-
Label (上記を参照)にジャンプすることによって、DCL プログラムの実行の流れを変更するのに使用します。
-
SetTagSyncBits
-
データストリームに送信するパケットのアイソクロナスパケットヘッダのタグと同期ビットを指定します。
-
TimeStamp
-
このコマンドは、タイムスタンプ変数へのポインタ(32 ビット値へのポインタ)を受け取ります。変数は、
TimeStamp コマンドが UpdateDCLList コマンド(上記を参照)によって更新されると埋められます。
タイムスタンプは、「1394 Open Host Controller Interface Specification v1.1」のセクション 7.1.5.3 で定義されている 16 ビット値です。
図 1. タイムスタンプ
タイムスタンプ変数にコピーされるとき、上の図 1 に示した 16 ビットの値は、上位 16 ビットに配置されます。タイムスタンプ変数の下位 16 ビットはゼロに設定されます。
-
UpdateDCLList
-
このコマンドは、
Send 、Receive 、または TimeStamp コマンドのリストを更新するのに使います。たとえば、パケットヘッダのバイトスワップを行うことによって受信バッファまたは送信バッファを準備し、TimeStamp コマンドによって示されるタイムスタンプにタイムスタンプ情報をコピーします。
更新は CPU によって実行されます。更新を実行している間、DCL プログラムは CPU とは独立に実行を続けます。
初めて送信 DCL コマンドを実行するときには、更新のステップは必要ありません。これは、FireWire ソフトウェアが、DCL プログラムを起動する前に更新を実行するためです。しかし、もう一度実行する前には、送信 DCL コマンドを更新する必要があります。
受信 DCL は、バッファを参照する前に必ず更新する必要があります。更新によって、バイトスワップやエンディアンの問題が処理されます。
タイムスタンプ DCL は、タイムスタンプを読み込む前に必ず更新する必要があります。更新処理によって、タイムスタンプが生成されます。
-
- 実装されていないコマンド
-
いくつかの DCL コマンドは現在実装されていませんが、
IOFireWireLibDCLCommandPool インタフェースにそれらを割り当てるメソッドはあります。そのため、DCL プログラムを作成する際には、以下のメソッドを使用しないでください。
AllocateTransferBufferDCL
AllocateReceiveBufferDCL
AllocateSendBufferDCL
先頭に戻る
DCL プログラムの構造と実行
DCL プログラムは本質的に、DCL コマンドで構成されているリンクリストです。プログラムは FireWire ハードウェア上でコンパイルされ、CPU とは独立に実行されます。つまり、DCL プログラムはハードウェア内でリアルタイムで動作します。これにより、「DCL プログラムの組み立て方」のセクションでさらに説明するようないくつかの前提条件が生まれます。
DCL プログラムは、リンクリストの最初の DCL コマンドから開始して、リストの順番で実行されます。実行の順番は、プログラムの実行を特定の Label に移行できる Jump コマンドを使って変更できます。Jump コマンドと Label コマンドを使って非常に複雑なプログラムを作成できるほか、プログラムを実行している最中にも Jump コマンドの宛先を変更することもできます。Jump コマンドと Label コマンドの使い方については、「DCL プログラムの組み立て方」のセクションで詳しく説明します。
各 DCL コマンドには、プログラム中の次の DCL コマンドを指す pNextDCLCommand フィールドがあります。DCL プログラムの終了を示すには、プログラムの最後の DCL コマンドの pNextDCLCommand をゼロに設定します。最後の DCL コマンドがプログラムの先頭に戻る Jump であっても、プログラムの終了を示すには pNextDCLCommand フィールドをゼロに設定する必要があります。この pNextDCLCommand フィールドをゼロにしないと、DCL コンパイラがハングする場合があります。
注:
通常の DCL プログラムには、いくつかの ReceivePacketStart コマンドや SendPacketStart コマンドが含まれます。ReceivePacket コマンドと SendPacket コマンドは、使用できますが、使用しないのが一般的です。
|
DCL プログラムを作成する際には、従うべき基本ルールが 3 つあります。
-
Label コマンドの直後には、SendPacketStart コマンドまたは ReceivePacketStart コマンドを続けます。
-
UpdateDCLList コマンドと CallProc コマンドは、SendPacketStart コマンドか ReceivePacketStart コマンドの直後、あるいは、SendPacketStart コマンドまたは ReceivePacketStart コマンドに続く UpdateDCLList コマンドか CallProc コマンドの直後に配置します。
- 最良の結果を出すために、パケットバッファはページの境界を越えないようにします(詳細は後述)。
簡単な例:
パケットを受信する簡単な DCL プログラムは、Label 、いくつかの ReceivePacketStart コマンド、UpdateDCLList 、CallProc で構成されます。たとえば、次のようになります。
Label
ReceivePacketStart
ReceivePacketStart
ReceivePacketStart
ReceivePacketStart
ReceivePacketStart
UpdateDCLList
CallProc
|
アドレス DCL コマンド pNexDCLCommand が指している先
0001 Label 0002
0002 ReceivePacketStart 0003
0003 ReceivePacketStart 0004
0004 ReceivePacketStart 0005
0005 ReceivePacketStart 0006
0006 ReceivePacketStart 0007
0007 UpdateDCLList 0008
0008 CallProc NULL (DCL プログラムの終わり)
|
この DCL プログラムは 5 つのパケットを受信し、エンディアン問題を処理するために受信したデータを更新したあと、5 つのパケットを受信して読み込む準備ができたことをコードに知らせるユーザ指定のコールバック関数を呼び出します。
ループの例:
もう少し複雑な例として、Jump コマンドを使ってループを作成することによって、データストリームからデータを連続して受信するプログラムを組み立てます。
Label1
ReceivePacketStart
(198 個の ReceivePacketStart コマンド)
ReceivePacketStart
UpdateDCLList
CallProc
Jump Label1
|
この DCL プログラムは、合計 200 パケット(合計 200 個の ReceivePacketStart コマンド)を受信して、受信したデータを更新し、ユーザ指定のコールバック関数を呼び出したあと、Label1 にループバックし、もう一度データの受信を始めます。
重要:
上記の DCL プログラムはループを示す単なる例であって、データを受信するための実践的な実装ではありません。詳細については、「DCL プログラムの組み立て方」のセクションを参照してください。
|
先頭に戻る
バッファの割り当て
パケットにバッファを割り当てるときには、バッファを作成する vm_allocate と、使い終わったときにバッファを解放する vm_deallocate を使用します。また、DCL データバッファがページ境界を越えなければ、最高のパフォーマンスが得られます。
ページ境界を考慮すると、仮想メモリページに収まるパケットの数を計算し、メモリを単一ブロックとして割り当て、それをパケットバッファに分割するのが良い方法です。vm_allocate では、必ずページに合わせたメモリが返されるため、メモリのページ境界合わせについて心配する必要はありません。
仮想メモリページはどのくらいの大きさになるのでしょうか。getpagesize (unistd.h で定義)を使って割り当てサイズを計算し、バッファがページの境界を越えないように境界を合わせることができます。
例:
500 のバッファが必要であり、(アイソクロナスヘッダを含めて)最大 800 バイトの大きさのパケットを受信しているために、各バッファは 800 バイトになると想定します。このとき単に 500 x 800 の配列を割り当てたとしたら、多くのバッファはページ境界を越えることになります。現在、仮想メモリページは 4096 (4KB) であるため、4KB のページにうまく収まる 800 バイトのバッファ数がいくつになるかを次のようにして計算できます。
4096 / 8 = 5 バッファ数/ページ (各ページに 96 バイト残す)
500 バッファ / 5 バッファ/ページ = 100 ページ
|
vm_allocate はページ境界に合わせてメモリを割り当てることが保証されているため、返されたメモリのページ境界を心配する必要はありません。
リスト 1. ページ境界に合わせたバッファの割り当て
| kern_return_t result;
vm_address_t p, savePtr;
vm_size_t allocSize;
allocSize = 100 * getpagesize(); // メモリ割り当てのサイズ
// 任意のアドレスにメモリを割り当てる
result = vm_allocate( current_task(),
&p,
allocSize,
FALSE );
if( KERN_SUCCESS == result )
{
savePtr = p; // あとで vm_deallocate での使用のためにポインタを保存
// これで p はページ境界に合ったアドレスから始まる 100 ページを指している
...
}
...
// すべて完了したら
result = vm_deallocate( current_task(), savePtr, allocSize );
|
先頭に戻る
受信データ
受信バッファのデータは、4 バイトのアイソクロナスパケットヘッダと、それに続く受信パケットからのデータペイロードで構成されます。アイソクロナスヘッダとペイロードの CRC となる 4 バイト分のデータは、受信バッファに置かれません。
つまり、各パケットの受信バッファには、ペイロードサイズ+ 4 バイトの長さが必要です。
先頭に戻る
送信データ
SendPacketStart コマンドを使用する場合は、アイソクロナスパケットヘッダが生成され、送信バッファのデータがそのパケットのペイロードになります。
SendPacketWithHeaderStart を使用する場合は、自分でアイソクロナスパケットヘッダを生成する必要があります。このパケットヘッダを送信バッファの最初の 4 バイトとして、その直後にペイロードデータを続けます。
どちらの場合も、アイソクロナスパケットヘッダとペイロードの CRC となる 4 バイトは自動的に生成されます。
先頭に戻る
DCL プログラムの組み立て方
一般に、連続したデータのストリームの場合は、DCL プログラムを複数のセグメントに分割します。CallProc の実行と並行して、FireWire ハードウェアでは DCL プログラムが実行を続けるからです。このため、CallProc の実行中に着信パケットの受信を続けるのに十分なバッファを用意する必要があります。
FireWire ハードウェアは、着信パケットを直接バッファに DMA 転送します。DCL プログラムに 1 つ以上のループがある場合は、DMA の処理が循環して、すでに受信したデータが上書きされる危険があります。これは、CallProc で遅延が生じたり、CallProc に時間をかけすぎたりした場合に生じます。
オーバーランの発生を検出する仕組みを提供する良い DCL プログラムの例を以下に示します。
Label 1
ReceivePacketStartOp x 200
Timestamp (省略可能)
Update (200 パケット + タイムスタンプ)
CallProc A
Jump A -> Label 2
Label 2
ReceivePacketStartOp x 200
Timestamp (省略可能)
Update (200 パケット + タイムスタンプ)
CallProc B
Jump B -> Label 3
Label 3
ReceivePacketStartOp x 200
Timestamp (省略可能)
Update (200 パケット + タイムスタンプ)
CallProc C
Jump C -> Label 4
Label 4
ReceivePacketStartOp (上記のルールに従って、1 回のみ)
CallProc D
(end = 次の DCL へのリンク == NULL)
|
プログラムの動作の仕組み
CallProc A が実行されると、必要なデータ処理が実行され、次に 2 つの Jump コマンドが変更されます。Jump A が Label 4 を指すように、Jump C が Label 1 を指すように変更されます。
Label 1
ReceivePacketStartOp x 200
Timestamp (省略可能)
Update (200 パケット + タイムスタンプ)
CallProc A
-> Jump A -> Label 4
Label 2
ReceivePacketStartOp x 200
Timestamp (省略可能)
Update (200 パケット + タイムスタンプ)
CallProc B
Jump B -> Label 3
Label 3
ReceivePacketStartOp x 200
Timestamp (省略可能)
Update (200 パケット + タイムスタンプ)
CallProc C
-> Jump C -> Label 1
Label 4
ReceivePacketStartOp (上記のルールに従って、1 回のみ)
CallProc D
(end = 次の DCL へのリンク == NULL)
|
この処理は、実行直前に Jump A を変更しているように見えますが、実際は違います。DCL プログラムは実行を続けているため、Jump A をはるかに過ぎてから CallProc が呼び出されます。
CallProc B が実行されると、必要なデータ処理が実行され、次に 2 つの Jump コマンドが変更されます。Jump A が Label 2 を指すように、Jump B が Label 4 を指すように変更します。
Label 1
ReceivePacketStartOp x 200
Timestamp (省略可能)
Update (200 パケット + タイムスタンプ)
CallProc A
-> Jump A -> Label 2
Label 2
ReceivePacketStartOp x 200
Timestamp (省略可能)
Update (200 パケット + タイムスタンプ)
CallProc B
-> Jump B -> Label 4
Label 3
ReceivePacketStartOp x 200
Timestamp (省略可能)
Update (200 パケット + タイムスタンプ)
CallProc C
Jump C -> Label 1
Label 4
ReceivePacketStartOp (上記のルールに従って、1 回のみ)
CallProc D
(end = 次の DCL へのリンク == NULL)
|
CallProc C が実行されると、必要なデータ処理が実行され、次に 2 つの Jump コマンドが変更されます。Jump B は Label 3 を指すように、Jump C が Label 4 を指すように変更されます。
Label 1
ReceivePacketStartOp x 200
Timestamp (省略可能)
Update (200 パケット + タイムスタンプ)
CallProc A
Jump A -> Label 2
Label 2
ReceivePacketStartOp x 200
Timestamp (省略可能)
Update (200 パケット + タイムスタンプ)
CallProc B
-> Jump B -> Label 3
Label 3
ReceivePacketStartOp x 200
Timestamp (省略可能)
Update (200 パケット + タイムスタンプ)
CallProc C
-> Jump C -> Label 4
Label 4
ReceivePacketStartOp (上記のルールに従って、1 回のみ)
CallProc D
(end = 次の DCL へのリンク == NULL)
|
この手法は、CallProc が実行されるたびに、DCL プログラムを結果的に「拡張」します。CallProc の実行のたびに、プログラムの末尾を現在実行している箇所からさらに先に移動しているという考え方もできます。
CallProc D が呼び出された場合には、ソフトウェアの動作が遅れたために、DCL プログラムによって受信バッファが使い果たされたことがわかります。これによって循環の発生が検出され、適切な措置を取れることが保証されます。
上記の例は、パケットを送信する DCL プログラムに同じように応用できます。
先頭に戻る
可変サイズのパケットの受信
一部のアイソクロナスストリームは、パケットサイズが可変になります。このようなパケットを受信するには、予想される最大のパケットを受信できる十分な大きさの受信バッファを作成します。各バッファは 1 つのパケットを受信します。着信する各パケットの先頭の 4 バイトには、アイソクロナスヘッダが含まれています。このヘッダを見ることで、受信する各パケットの実際のペイロードサイズを確認できます。このヘッダを調べる前に、DCL プログラムで UpdateDCLList コマンドを実行するようにしてください。
先頭に戻る
可変サイズのパケットの送信
現在、Mac OS X は、送信 DCL のデータサイズのその場での変更はサポートしていません。可変サイズのパケットを送信する必要がある場合は、送信する全パケットの順番とサイズがあらかじめ分かっていれば実現できます。複数のセグメントからなり、各セグメントが異なるパケットサイズに対応するような DCL プログラムを作成します。CallProc の間の Jump DCL のジャンプ先を慎重に操作することによって、可変サイズのパケットからなるストリームを作成できます。
先頭に戻る
スレッドについて
効率的にデータの送受信を行うには、DCL CallProc をタイミング良くスケジューリングすることが不可欠です。これを確実に行う手法として、DCL CallProc を個別のリアルタイムスレッドで実行するやり方があります。
-
送受信に使用するスレッドを作成します。各スレッドがそれぞれの CFRunLoop を持つことになります。
-
AddCallbackDispatcherToRunLoop メソッドと AddIsochCallbackDispatcherToRunLoop メソッドを使って、スレッドの CFRunLoop にコールバックディスパッチャを追加します。
-
Mach Thread API の
thread_policy_set を使って、スレッドのプライオリティがリアルタイムスレッドになるように変更します。thread_policy_set の使用方法については、「Inside Mac OS X: Kernel Progamming」の「Mach Scheduling and Thread Interfaces」の章の「Using the Mach Thread API to Influence Scheduling」のセクションを参照してださい。
-
CFRunLoopRun を呼び出して、スレッドの runloop を実行します。
先頭に戻る
要約
DCL プログラムは扱いが難しい場合があります。プログラムの構造、バッファの割り当て、CallProc の適切な処理が、効率的で堅牢な DCL プログラムを作成する上で非常に重要になります。本稿で説明したテクニックを応用することによって、多くの頭痛の種を回避でき、DCL プログラムを期待どおりに動作させることができます。
先頭に戻る
参考資料
先頭に戻る
|